﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LowLevelGraphics.Filter
{
    /// <summary>
    /// Adaptive Logarithmic Mapping For Displaying High Contrast Scenes
    /// F. Drago, K. Myszkowski, T. Annen, N. Chiba
    /// 
    /// TODO: finish
    /// </summary>
    public sealed class Drago : Operator
    {
        private const float m_fBIAS = 0.85f;

        /// <summary>
        /// Gets/sets exposure
        /// </summary>
        public double Exposure
        {
            get { return this.m_dExposure; }
            set { this.m_dExposure = value; }
        }

        /// <summary>
        /// Gets or sets gamme
        /// </summary>
        public double Gamma
        {
            get { return this.m_dGamma; }
            set { this.m_dGamma = Math.Min(Math.Max(value, 0.2), 5); }
        }

        private double m_dExposure = 0.0;
        private double m_dGamma = 1.0;

        /// <summary>
        /// Default constructor
        /// </summary>
        public Drago()
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="_dExposure"></param>
        /// <param name="_dGamma"></param>
        public Drago(double _dExposure, double _dGamma)
        {
            this.Exposure = _dExposure;
            this.Gamma = _dGamma;
        }

        /// <summary>
        /// Run
        /// </summary>
        /// <param name="mapInput"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="mapOutput"></param>
        protected override void Run(float[][,] mapInput, int width, int height, ref float[][,] mapOutput)
        {
            // default parameters
            float exposure = (float)Math.Pow(2.0, this.m_dExposure);

            // convert to Yxy
            ConvertRGBToYxy(mapInput, ref mapOutput);

            // get the luminance
            float maxLum = 0, avgLum = 0;

            LuminanceFromYxy(mapOutput, ref maxLum, ref avgLum);

            // perform tone mapping
            Tonemap(mapOutput, maxLum, avgLum, exposure);

            // convert to RGB
            ConvertYxyToRGB(mapOutput);

            // apply gamma correction
            if (m_dGamma != 1)
                CorrectGamma(mapOutput, m_dGamma);
        }

        /// <summary>
        /// Set Param
        /// </summary>
        /// <param name="param"></param>
        /// <param name="value"></param>
        protected override void SetParam(string param, string value)
        {
            switch (param)
            {
                case "exposure":
                    this.Exposure = Double.Parse(value);
                    break;
                case "gamma":
                    this.Gamma = Double.Parse(value);
                    break;
            }
        }

        /// <summary>
        /// Convert RGB to xy
        /// </summary>
        /// <param name="_aMapInput"></param>
        /// <param name="_aMapOutput"></param>
        private void ConvertRGBToYxy(float[][,] _aMapInput, ref float[][,] _aMapOutput)
        {
            int width = _aMapInput[0].GetLength(0);
            int height = _aMapInput[0].GetLength(1);

            int x = 0, y = 0;
            ColorSpaceConverter converter = new ColorSpaceConverter();
            double R = 0, G = 0, B = 0, X = 0, Y = 0, Z = 0, Y_ = 0, x_ = 0, y_ = 0;

            for (y = 0; y < height; y++)
                for (x = 0; x < width; x++)
                {

                    R = _aMapInput[2][x, y];
                    G = _aMapInput[1][x, y];
                    B = _aMapInput[0][x, y];

                    converter.RGBToXYZ(R, G, B, ref X, ref Y, ref Z);
                    converter.XYZToYxy(X, Y, Z, ref Y_, ref x_, ref y_);

                    _aMapOutput[2][x, y] = (float)Y_;
                    _aMapOutput[1][x, y] = (float)x_;
                    _aMapOutput[0][x, y] = (float)y_;
                }
        }

        /// <summary>
        /// Luminance from XY
        /// </summary>
        /// <param name="map"></param>
        /// <param name="maxLum"></param>
        /// <param name="avgLum"></param>
        private void LuminanceFromYxy(float[][,] map, ref float maxLum, ref float avgLum)
        {
            int width = map[0].GetLength(0);
            int height = map[0].GetLength(1);

            maxLum = Single.MinValue;

            float val = 0;
            double sum = 0;
            int x = 0, y = 0;

            for (y = 0; y < height; y++)
            {
                for (x = 0; x < width; x++)
                {

                    val = map[0][x, y];

                    if (val > maxLum)
                        maxLum = val;

                    sum += val;
                }
            }

            avgLum = (float)(sum / (width * height));
        }

        /// <summary>
        /// Tonemap
        /// </summary>
        /// <param name="_aMap"></param>
        /// <param name="_fMaxLum"></param>
        /// <param name="_fAvgLum"></param>
        /// <param name="_fExposure"></param>
        private void Tonemap(float[][,] _aMap, float _fMaxLum, float _fAvgLum, float _fExposure)
        {
            int width = _aMap[0].GetLength(0);
            int height = _aMap[0].GetLength(1);
            int x = 0, y = 0;

            double log05 = Math.Log(0.5);

            for (y = 0; y < height; y++)
                for (x = 0; x < width; x++)
                {

                    //map[0][x, y] += (float)Math.Pow(map[0][x, y], 1.5);
                }

            /*
            float log05 = (float)Math.Log(0.5);

            double LMax = 0, divider = 0, interpol = 0, biasP = 0, L = 0;
            int x = 0, y = 0;
            double Yw = 0;

            // normalize maximum luminance by average luminance
            LMax = (maxLum / avgLum);

            divider = Math.Log10(LMax + 1);
            biasP = (Math.Log(BIAS) / log05);

            float min = Single.MaxValue;
            float max = Single.MinValue;

            for (y = 0; y < height; y++)
              for (x = 0; x < width; x++) {

                if (map[0][x, y] < min)
                  min = map[0][x, y];

                if (map[0][x, y] > max)
                  max = map[0][x, y];

                Yw = (map[0][x, y] / avgLum);

                if (exposure != 1.0)
                  Yw *= exposure;

                interpol = Math.Log(2 + GetBias(biasP, Yw / LMax) * 8);
                L = Math.Log(Yw + 1);//PadeLog(Yw);

                map[0][x, y] = (float)((L / interpol) / divider);
              }
             */
        }

        /// <summary>
        /// Get Bias
        /// </summary>
        /// <param name="b"></param>
        /// <param name="x"></param>
        /// <returns></returns>
        private double GetBias(double b, double x)
        {
            return Math.Pow(x, b);
        }

        /// <summary>
        /// Pade Log
        /// </summary>
        /// <param name="x"></param>
        /// <returns></returns>
        private double PadeLog(double x)
        {
            if (x < 1)
                return (x * (x + 6) / (x * 4 + 6));

            if (x < 2)
                return (x * (x * 0.7662 + 6) / (x * 3.7658 + 5.9897));

            return Math.Log(x + 1);
        }

        /// <summary>
        /// Correct Gamma
        /// </summary>
        /// <param name="map"></param>
        /// <param name="gamma"></param>
        private void CorrectGamma(float[][,] map, double gamma)
        {
            int width = map[0].GetLength(0);
            int height = map[0].GetLength(1);

            int x = 0, y = 0;
            float val = 0;

            float slope = 4.5f;
            float start = 0.018f;
            float fGamma = (float)((0.45 / gamma) * 2);

            if (gamma >= 2.1f)
            {

                start = (float)(0.018 / ((gamma - 2) * 7.5));
                slope = (float)(4.5 * ((gamma - 2) * 7.5));
            }
            else if (gamma <= 1.9f)
            {

                start = (float)(0.018 * ((2 - gamma) * 7.5));
                slope = (float)(4.5 / ((2 - gamma) * 7.5));
            }

            for (int c = 0; c < 3; c++)
                for (y = 0; y < height; y++)
                    for (x = 0; x < width; x++)
                    {

                        val = map[c][x, y];

                        if (val <= start)
                            val *= slope;
                        else
                            val = (float)(1.099 * Math.Pow(val, fGamma) - 0.099);

                        map[c][x, y] = Math.Min(Math.Max(val, 0), 1);
                    }
        }

        /// <summary>
        /// Convert YxyToRGB
        /// </summary>
        /// <param name="map"></param>
        private void ConvertYxyToRGB(float[][,] map)
        {

            int width = map[0].GetLength(0);
            int height = map[0].GetLength(1);

            int x = 0, y = 0;
            ColorSpaceConverter converter = new ColorSpaceConverter();
            double R = 0, G = 0, B = 0, Y_ = 0, x_ = 0, y_ = 0, X = 0, Y = 0, Z = 0;

            for (y = 0; y < height; y++)
            {
                for (x = 0; x < width; x++)
                {

                    Y_ = map[2][x, y];
                    x_ = map[1][x, y];
                    y_ = map[0][x, y];

                    converter.YxyToXYZ(Y_, x_, y_, ref X, ref Y, ref Z);
                    converter.XYZToRGB(X, Y, Z, ref R, ref G, ref B);

                    map[2][x, y] = (float)R;
                    map[1][x, y] = (float)G;
                    map[0][x, y] = (float)B;
                }
            }
        }
    }
}
